本篇我们主要分析AQS独占模式的源码,关于AQS的独占模式我们上一篇有所介绍。主要这里我们介绍acquire和release部分的代码。这会涉及到AQS的阻塞唤醒机制,还有其维护的FIFO队列。
阻塞过程
独占模式下的阻塞过程
1 | public final void acquire(int arg) { |
tryAcquire返回boolean值,true代表状态更新成功线程继续,否则当前线程需要阻塞,并添加到队列中。这里addWaiter会为当前线程创建Nodj节点并添加到队列中。因为是独占模式节点模式为Node.EXCLUSIVE。
1 | private Node addWaiter(Node mode) { |
addWaiter中首先为线程创建node节点,如果tail不为null说明队列不为空,这里先尝试通过CAS将node添加到队尾,然后返回,如果CAS尝试失败,则通过调用enq添加。
1 | private Node enq(final Node node) { |
首先enq是一个for循环,这里可以保证node节点一定可以添加到等待队列。首先判断队列是否为空,如果是则new一个Node节点作为当前空队列的头节点,同时将尾节点也指向它,然后在下个循环中同样通过CAS添加当前node节点到队列中。即使失败也通过循环再次尝试添加,直到成功。
我们看看AQS中队列的节点信息
1 | /** |
AQS队列是通过head和tail节点来维护的,其中Node节点分别有前驱和后继节点。
它是一个简单的队列结构,而保证线程节点能够正确添加到队列中正是基于CAS,这使得它是一个非阻塞式的队列。
1 | final boolean acquireQueued(final Node node, int arg){ |
添加Node到队列中后,有可能会阻塞当前线程,这里获取当前node的前驱,这个前驱节点如果等于head,说明队列中的头节点所代表的线程已经执行完毕release了,当前线程是从parck中解放出来的,这时候当前线程需要通过tryAcquire更新抢占锁,如果抢到了就将当前线程的node节点作为队列的头节点即head,head节点代表的线程是当前正在运行的线程。当然如果p不等于head了,说明它前面还有等待的线程再等待,需要注意的这个队列是一个非阻塞的FIFO队列,所以它需要等待前面的线程执行完毕,因此进入通过shouldParkAfterFailedAcquire判断是否应该park当前线程,返回true表示应该阻塞线程,然后通过parkAndCheckInterrupt来阻塞当前线程。
1 | private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) { |
这里只有当node的前驱节点的waitStatus为Node.SIGNAL时才会返回true,此时表示前驱节点会unPark node节点的线程,所以可以park这个线程。如果前驱节点不满足这个条件,就需要查找一个不为CANCELED的节点作为node的前驱,并更新它的waitStatus为SIGNAL。
1 | private final boolean parkAndCheckInterrupt() { |
线程的阻塞是在parkAndCheckInterrupt中进行的,阻塞使用了LockSupport的park。这样当前线程就阻塞在acquireQueued的for循环中等待被唤醒。
共享模式下的阻塞过程
1 | public final void acquireShared(int arg) { |
在共享模式下的逻辑类似于独占模式,tryAcquireShared返回负值代表未获取到同步状态需要阻塞,这里是通过doAcquireShared来完成的。
1 | private void doAcquireShared(int arg) { |
当子类的实现tryRelease返回true表示释放了同步状态,这时候就可以唤醒当前node节点所代表线程的后继节点了。这一步是通过unparkSuccessor实现的。
1 | private void unparkSuccessor(Node node) { |
这里取到当前队列的下一个node节点,并通过LockSupport.unpark解除相应线程的阻塞状态。